/*
 * SonarQube
 * Copyright (C) 2009-2023 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

import { act, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { omit, pick } from 'lodash';
import * as React from 'react';
import { byRole, byText } from 'testing-library-selector';
import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock';
import { KeyboardKeys } from '../../../helpers/keycodes';
import { mockIssueComment } from '../../../helpers/mocks/issues';
import { mockIssue, mockLoggedInUser, mockRawIssue } from '../../../helpers/testMocks';
import { findTooltipWithContent, renderApp } from '../../../helpers/testReactTestingUtils';
import {
  IssueActions,
  IssueSeverity,
  IssueStatus,
  IssueTransition,
  IssueType,
} from '../../../types/issues';
import { RuleStatus } from '../../../types/rules';
import { IssueComment } from '../../../types/types';
import Issue from '../Issue';

jest.mock('../../../api/issues');
jest.mock('../../../api/rules');
jest.mock('../../../api/users');

jest.mock('../../../helpers/preferences', () => ({
  getKeyboardShortcutEnabled: jest.fn(() => true),
}));

const issuesHandler = new IssuesServiceMock();

beforeEach(() => {
  issuesHandler.reset();
});

describe('rendering', () => {
  it('should render correctly with comments', () => {
    const { ui } = getPageObject();
    renderIssue({ issue: mockIssue(false, { comments: [mockIssueCommentPosted4YearsAgo()] }) });

    const comments = within(ui.commentsList());
    expect(comments.getByText('Leïa Skywalker')).toBeInTheDocument();
    expect(comments.getByRole('listitem')).toHaveTextContent('This is a comment, bud');
    expect(comments.getByRole('listitem')).toHaveTextContent('issue.comment.posted_on4 years ago');
  });

  it('should render correctly for locations, issue message, line, permalink, why, and effort', async () => {
    const { ui } = getPageObject();
    const issue = mockIssue(true, { effort: '2 days', message: 'This is an issue' });
    const onClick = jest.fn();
    renderIssue({ issue, displayLocationsCount: true, displayWhyIsThisAnIssue: true, onClick });

    expect(ui.locationsBadge(7).get()).toBeInTheDocument();
    expect(ui.lineInfo(26).get()).toBeInTheDocument();
    expect(ui.permalink.get()).toHaveAttribute(
      'href',
      `/project/issues?issues=${issue.key}&open=${issue.key}&id=${issue.project}`
    );
    expect(ui.whyLink.get()).toBeInTheDocument();
    expect(ui.effort('2 days').get()).toBeInTheDocument();
    await ui.clickIssueMessage();
    expect(onClick).toHaveBeenCalledWith(issue.key);
  });

  it.each([RuleStatus.Deprecated, RuleStatus.Removed])(
    'should render correctly for a %s rule',
    (ruleStatus) => {
      const { ui } = getPageObject();
      renderIssue({ issue: mockIssue(false, { ruleStatus }) });
      expect(ui.ruleStatusBadge(ruleStatus).get()).toBeInTheDocument();
    }
  );

  it('should render correctly for external rule engines', () => {
    renderIssue({ issue: mockIssue(true, { externalRuleEngine: 'ESLINT' }) });
    expect(screen.getByText('ESLINT')).toBeInTheDocument();
  });

  it('should render the SonarLint icon correctly', () => {
    renderIssue({ issue: mockIssue(false, { quickFixAvailable: true }) });
    expect(
      findTooltipWithContent('issue.quick_fix_available_with_sonarlint_no_link')
    ).toBeInTheDocument();
  });

  it('should render correctly with a checkbox', async () => {
    const { ui } = getPageObject();
    const onCheck = jest.fn();
    const issue = mockIssue();
    renderIssue({ onCheck, issue });
    await ui.toggleCheckbox();
    expect(onCheck).toHaveBeenCalledWith(issue.key);
  });

  it('should correctly render the changelog', async () => {
    const { ui } = getPageObject();
    renderIssue();

    await ui.showChangelog();
    expect(
      ui.changelogRow('status', IssueStatus.Confirmed, IssueStatus.Reopened).get()
    ).toBeInTheDocument();
    expect(ui.changelogRow('assign', 'luke.skywalker', 'darth.vader').get()).toBeInTheDocument();
  });
});

describe('updating', () => {
  it('should allow updating the type', async () => {
    const { ui } = getPageObject();
    const issue = mockRawIssue(false, {
      type: IssueType.Bug,
      actions: [IssueActions.SetType],
    });
    issuesHandler.setIssueList([{ issue, snippets: {} }]);
    renderIssue({ issue: mockIssue(false, { ...pick(issue, 'actions', 'key', 'type') }) });

    await ui.updateType(IssueType.Bug, IssueType.CodeSmell);
    expect(ui.updateTypeBtn(IssueType.CodeSmell).get()).toBeInTheDocument();
  });

  it('should allow updating the severity', async () => {
    const { ui } = getPageObject();
    const issue = mockRawIssue(false, {
      severity: IssueSeverity.Blocker,
      actions: [IssueActions.SetSeverity],
    });
    issuesHandler.setIssueList([{ issue, snippets: {} }]);
    renderIssue({ issue: mockIssue(false, { ...pick(issue, 'actions', 'key', 'severity') }) });

    await ui.updateSeverity(IssueSeverity.Blocker, IssueSeverity.Minor);
    expect(ui.updateSeverityBtn(IssueSeverity.Minor).get()).toBeInTheDocument();
  });

  it('should allow updating the status', async () => {
    const { ui } = getPageObject();
    const issue = mockRawIssue(false, {
      status: IssueStatus.Open,
      transitions: [IssueTransition.Confirm, IssueTransition.UnConfirm],
    });
    issuesHandler.setIssueList([{ issue, snippets: {} }]);
    renderIssue({
      issue: mockIssue(false, { ...pick(issue, 'key', 'status', 'transitions') }),
    });

    await ui.updateStatus(IssueStatus.Open, IssueTransition.Confirm);
    expect(ui.updateStatusBtn(IssueStatus.Confirmed).get()).toBeInTheDocument();
  });

  it('should allow assigning', async () => {
    const { ui } = getPageObject();
    const issue = mockRawIssue(false, {
      assignee: 'leia',
      actions: [IssueActions.Assign],
    });
    issuesHandler.setIssueList([{ issue, snippets: {} }]);
    renderIssue({
      issue: mockIssue(false, { ...pick(issue, 'actions', 'key', 'assignee') }),
    });

    await ui.updateAssignee('leia', 'Skywalker');
    expect(ui.updateAssigneeBtn('luke').get()).toBeInTheDocument();
  });

  it('should allow commenting', async () => {
    const { ui } = getPageObject();
    const issue = mockRawIssue(false, {
      actions: [IssueActions.Comment],
    });
    issuesHandler.setIssueList([{ issue, snippets: {} }]);
    renderIssue({ issue: mockIssue(false, { ...pick(issue, 'actions', 'key') }) });

    // Create
    await ui.addComment('Original content');
    const comments = within(ui.commentsList());
    expect(comments.getByRole('listitem')).toHaveTextContent('Original content');

    // Update
    await ui.updateComment('New content');
    expect(comments.getByRole('listitem')).toHaveTextContent('New content');

    // Delete
    await ui.deleteComment();
    expect(comments.getByRole('listitem')).toHaveTextContent('New content');
  });

  it('should allow updating the tags', async () => {
    const { ui } = getPageObject();
    const issue = mockRawIssue(false, {
      tags: [],
      actions: [IssueActions.SetTags],
    });
    issuesHandler.setIssueList([{ issue, snippets: {} }]);
    renderIssue({ issue: mockIssue(false, { ...pick(issue, 'actions', 'key', 'tags') }) });

    await ui.addTag('accessibility');
    await ui.addTag('android', ['accessibility']);
    expect(ui.updateTagsBtn(['accessibility', 'android']).get()).toBeInTheDocument();
  });
});

it('should correctly handle keyboard shortcuts', async () => {
  const { ui } = getPageObject();
  const onCheck = jest.fn();
  const issue = mockRawIssue(false, {
    actions: Object.values(IssueActions),
    assignee: 'luke',
    transitions: [IssueTransition.Confirm, IssueTransition.UnConfirm],
  });
  issuesHandler.setIssueList([{ issue, snippets: {} }]);
  issuesHandler.setCurrentUser(mockLoggedInUser({ login: 'leia', name: 'Organa' }));
  renderIssue({
    onCheck,
    selected: true,
    issue: mockIssue(false, { ...pick(issue, 'actions', 'key', 'assignee', 'transitions') }),
  });

  await ui.pressTransitionShortcut();
  expect(ui.setStatusBtn(IssueTransition.UnConfirm).get()).toBeInTheDocument();
  await ui.pressDismissShortcut();

  await ui.pressAssignShortcut();
  expect(ui.setAssigneeBtn(/Organa/).get()).toBeInTheDocument();
  await ui.pressDismissShortcut();

  await ui.pressSeverityShortcut();
  expect(ui.setSeverityBtn(IssueSeverity.Minor).get()).toBeInTheDocument();
  await ui.pressDismissShortcut();

  await ui.pressCommentShortcut();
  expect(ui.commentTextInput.get()).toBeInTheDocument();
  await ui.pressDismissShortcut();

  await ui.pressTagsShortcut();
  expect(ui.tagsSearchInput.get()).toBeInTheDocument();
  await ui.pressDismissShortcut();

  await ui.pressCheckShortcut();
  expect(onCheck).toHaveBeenCalled();

  expect(ui.updateAssigneeBtn('luke').get()).toBeInTheDocument();
  await ui.pressAssignToMeShortcut();
  expect(ui.updateAssigneeBtn('leia').get()).toBeInTheDocument();
});

it('should correctly handle similar issues filtering', async () => {
  const { ui, user } = getPageObject();
  const onFilter = jest.fn();
  const issue = mockIssue(false, {
    ruleName: 'Rule Foo',
    tags: ['accessibility', 'owasp'],
    projectName: 'Project Bar',
    componentLongName: 'main.js',
  });
  renderIssue({ onFilter, issue });

  await ui.showSimilarIssues();
  await user.click(ui.similarIssueTypeLink.get());
  expect(onFilter).toHaveBeenLastCalledWith('type', issue);

  await ui.showSimilarIssues();
  await user.click(ui.similarIssueSeverityLink.get());
  expect(onFilter).toHaveBeenLastCalledWith('severity', issue);

  await ui.showSimilarIssues();
  await user.click(ui.similarIssueStatusLink.get());
  expect(onFilter).toHaveBeenLastCalledWith('status', issue);

  await ui.showSimilarIssues();
  await user.click(ui.similarIssueResolutionLink.get());
  expect(onFilter).toHaveBeenLastCalledWith('resolution', issue);

  await ui.showSimilarIssues();
  await user.click(ui.similarIssueAssigneeLink.get());
  expect(onFilter).toHaveBeenLastCalledWith('assignee', issue);

  await ui.showSimilarIssues();
  await user.click(ui.similarIssueRuleLink.get());
  expect(onFilter).toHaveBeenLastCalledWith('rule', issue);

  await ui.showSimilarIssues();
  await user.click(ui.similarIssueTagLink('accessibility').get());
  expect(onFilter).toHaveBeenLastCalledWith('tag###accessibility', issue);

  await ui.showSimilarIssues();
  await user.click(ui.similarIssueTagLink('owasp').get());
  expect(onFilter).toHaveBeenLastCalledWith('tag###owasp', issue);

  await ui.showSimilarIssues();
  await user.click(ui.similarIssueProjectLink.get());
  expect(onFilter).toHaveBeenLastCalledWith('project', issue);

  await ui.showSimilarIssues();
  await user.click(ui.similarIssueFileLink.get());
  expect(onFilter).toHaveBeenLastCalledWith('file', issue);
});

function mockIssueCommentPosted4YearsAgo(overrides: Partial<IssueComment> = {}) {
  const date = new Date();
  date.setFullYear(date.getFullYear() - 4);
  return mockIssueComment({
    authorName: 'Leïa Skywalker',
    createdAt: date.toISOString(),
    ...overrides,
  });
}

function getPageObject() {
  const user = userEvent.setup();

  const selectors = {
    // Issue
    ruleStatusBadge: (status: RuleStatus) => byText(`issue.resolution.badge.${status}`),
    locationsBadge: (count: number) => byText(count),
    lineInfo: (line: number) => byText(`L${line}`),
    permalink: byRole('link', { name: 'permalink' }),
    effort: (effort: string) => byText(`issue.x_effort.${effort}`),
    whyLink: byRole('link', { name: 'issue.why_this_issue.long' }),
    checkbox: byRole('checkbox'),
    issueMessageBtn: byRole('button', { name: 'This is an issue' }),

    // Changelog
    toggleChangelogBtn: byRole('button', {
      name: /issue.changelog.found_on_x_show_more/,
    }),
    changelogRow: (key: string, oldValue: string, newValue: string) =>
      byRole('row', {
        name: new RegExp(
          `issue\\.changelog\\.changed_to\\.issue\\.changelog\\.field\\.${key}\\.${newValue} \\(issue\\.changelog\\.was\\.${oldValue}\\)`
        ),
      }),

    // Similar issues
    toggleSimilarIssuesBtn: byRole('button', { name: 'issue.filter_similar_issues' }),
    similarIssueTypeLink: byRole('button', { name: 'issue.type.BUG' }),
    similarIssueSeverityLink: byRole('button', { name: 'severity.MAJOR' }),
    similarIssueStatusLink: byRole('button', { name: 'issue.status.OPEN' }),
    similarIssueResolutionLink: byRole('button', { name: 'unresolved' }),
    similarIssueAssigneeLink: byRole('button', { name: 'unassigned' }),
    similarIssueRuleLink: byRole('button', { name: 'Rule Foo' }),
    similarIssueTagLink: (name: string) => byRole('button', { name }),
    similarIssueProjectLink: byRole('button', { name: 'qualifier.TRK Project Bar' }),
    similarIssueFileLink: byRole('button', { name: 'qualifier.FIL main.js' }),

    // Comment
    commentsList: () => {
      const list = byRole('list')
        .getAll()
        .find((el) => el.getAttribute('data-testid') === 'issue-comments');
      if (list === undefined) {
        throw new Error('Could not find comments list');
      }
      return list;
    },
    commentAddBtn: byRole('button', { name: 'issue.comment.add_comment' }),
    commentEditBtn: byRole('button', { name: 'issue.comment.edit' }),
    commentTextInput: byRole('textbox', { name: 'issue.comment.enter_comment' }),
    commentSaveBtn: byRole('button', { name: 'issue.comment.formlink' }),
    commentUpdateBtn: byRole('button', { name: 'save' }),
    commentDeleteBtn: byRole('button', { name: 'issue.comment.delete' }),
    commentConfirmDeleteBtn: byRole('button', { name: 'delete' }),

    // Type
    updateTypeBtn: (currentType: IssueType) =>
      byRole('button', { name: `issue.type.type_x_click_to_change.issue.type.${currentType}` }),
    setTypeBtn: (type: IssueType) => byRole('button', { name: `issue.type.${type}` }),

    // Severity
    updateSeverityBtn: (currentSeverity: IssueSeverity) =>
      byRole('button', {
        name: `issue.severity.severity_x_click_to_change.severity.${currentSeverity}`,
      }),
    setSeverityBtn: (severity: IssueSeverity) => byRole('button', { name: `severity.${severity}` }),

    // Status
    updateStatusBtn: (currentStatus: IssueStatus) =>
      byRole('button', {
        name: `issue.transition.status_x_click_to_change.issue.status.${currentStatus}`,
      }),
    setStatusBtn: (transition: IssueTransition) =>
      byRole('button', { name: `issue.transition.${transition}` }),

    // Assignee
    assigneeSearchInput: byRole('searchbox'),
    updateAssigneeBtn: (currentAssignee: string) =>
      byRole('button', {
        name: `issue.assign.assigned_to_x_click_to_change.${currentAssignee}`,
      }),
    setAssigneeBtn: (name: RegExp) => byRole('button', { name }),

    // Tags
    tagsSearchInput: byRole('searchbox'),
    updateTagsBtn: (currentTags?: string[]) =>
      byRole('button', {
        name: `tags_list_x.${currentTags ? currentTags.join(', ') : 'issue.no_tag'}`,
      }),
    toggleTagCheckbox: (name: string) => byRole('checkbox', { name }),
  };

  const ui = {
    ...selectors,
    async addComment(content: string) {
      await user.click(selectors.commentAddBtn.get());
      await user.type(selectors.commentTextInput.get(), content);
      await act(async () => {
        await user.click(selectors.commentSaveBtn.get());
      });
    },
    async updateComment(content: string) {
      await user.click(selectors.commentEditBtn.get());
      await user.type(selectors.commentTextInput.get(), content);
      await act(async () => {
        await user.keyboard(`{Control>}{${KeyboardKeys.Enter}}{/Control}`);
      });
    },
    async deleteComment() {
      await user.click(selectors.commentDeleteBtn.get());
      await act(async () => {
        await user.click(selectors.commentConfirmDeleteBtn.get());
      });
    },
    async updateType(currentType: IssueType, newType: IssueType) {
      await user.click(selectors.updateTypeBtn(currentType).get());
      await act(async () => {
        await user.click(selectors.setTypeBtn(newType).get());
      });
    },
    async updateSeverity(currentSeverity: IssueSeverity, newSeverity: IssueSeverity) {
      await user.click(selectors.updateSeverityBtn(currentSeverity).get());
      await act(async () => {
        await user.click(selectors.setSeverityBtn(newSeverity).get());
      });
    },
    async updateStatus(currentStatus: IssueStatus, transition: IssueTransition) {
      await user.click(selectors.updateStatusBtn(currentStatus).get());
      await act(async () => {
        await user.click(selectors.setStatusBtn(transition).get());
      });
    },
    async updateAssignee(currentAssignee: string, newAssignee: string) {
      await user.click(selectors.updateAssigneeBtn(currentAssignee).get());
      await user.type(selectors.assigneeSearchInput.get(), newAssignee);
      await act(async () => {
        await user.click(selectors.setAssigneeBtn(new RegExp(newAssignee)).get());
      });
    },
    async addTag(tag: string, currentTagList?: string[]) {
      await user.click(selectors.updateTagsBtn(currentTagList).get());
      await act(async () => {
        await user.click(selectors.toggleTagCheckbox(tag).get());
      });
      await act(async () => {
        await user.keyboard('{Escape}');
      });
    },
    async showChangelog() {
      await user.click(selectors.toggleChangelogBtn.get());
    },
    async showSimilarIssues() {
      await user.click(selectors.toggleSimilarIssuesBtn.get());
    },
    async toggleCheckbox() {
      await user.click(selectors.checkbox.get());
    },
    async clickIssueMessage() {
      await user.click(selectors.issueMessageBtn.get());
    },
    async pressDismissShortcut() {
      await act(async () => {
        await user.keyboard(`{${KeyboardKeys.Escape}}`);
      });
    },
    async pressTransitionShortcut() {
      await act(async () => {
        await user.keyboard(`{${KeyboardKeys.KeyF}}`);
      });
    },
    async pressAssignShortcut() {
      await act(async () => {
        await user.keyboard(`{${KeyboardKeys.KeyA}}`);
      });
    },
    async pressAssignToMeShortcut() {
      await act(async () => {
        await user.keyboard(`{${KeyboardKeys.KeyM}}`);
      });
    },
    async pressSeverityShortcut() {
      await act(async () => {
        await user.keyboard(`{${KeyboardKeys.KeyI}}`);
      });
    },
    async pressCommentShortcut() {
      await act(async () => {
        await user.keyboard(`{${KeyboardKeys.KeyC}}`);
      });
    },
    async pressTagsShortcut() {
      await act(async () => {
        await user.keyboard(`{${KeyboardKeys.KeyT}}`);
      });
    },
    async pressCheckShortcut() {
      await act(async () => {
        await user.keyboard(`{${KeyboardKeys.Space}}`);
      });
    },
  };

  return { ui, user };
}

function renderIssue(props: Partial<Omit<Issue['props'], 'onChange' | 'onPopupToggle'>> = {}) {
  function Wrapper(wrapperProps: Omit<Issue['props'], 'onChange' | 'onPopupToggle'>) {
    const [issue, setIssue] = React.useState(wrapperProps.issue);
    const [openPopup, setOpenPopup] = React.useState<string | undefined>();
    return (
      <Issue
        issue={issue}
        openPopup={openPopup}
        onChange={(newIssue) => {
          setIssue({ ...issue, ...newIssue });
        }}
        onPopupToggle={(_key, popup, open) => {
          setOpenPopup(open === false ? undefined : popup);
        }}
        {...omit(wrapperProps, 'issue')}
      />
    );
  }

  return renderApp('/', <Wrapper issue={mockIssue()} selected={false} {...props} />, {
    currentUser: mockLoggedInUser({ login: 'leia', name: 'Organa' }),
  });
}
